#!/bin/bash
# Copyright (C) 2022 Checkmk GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.

set -e

JOBS=6
readonly ALL_LINTERS=mypy,ruff,bandit,semgrep

failure() {
    test ${#@} -eq 0 || echo "$(basename "$0"):" "$@" >&2
    exit 1
}

usage() {
    echo "usage: $(basename "$0") [OPTION]..."
    echo "Run the CI pipeline or parts of it."
    echo
    echo "  -u, --unit-tests        run unit tests"
    echo "  -F, --check-format      check for correct formatting"
    echo "  -l L,..., --lint=L,...  run linters, 'all' means '${ALL_LINTERS}'"
    echo "  -a, --all               shortcut for -u -F -l all"
    echo "  -f, --format            format sources"
    echo "  -D, --documentation     generate documentation"
    echo "  -j N, --jobs=N          allow N jobs at once, default is ${JOBS}"
    echo "  -h, --help              show this help"
}

parse_options() {
    # Yes, all those option variables are global.
    RUN_UNIT_TESTS=no
    RUN_CHECK_FORMAT=no
    RUN_MYPY=no
    RUN_RUFF=no
    RUN_BANDIT=no
    RUN_SEMGREP=no
    RUN_FORMAT=no
    RUN_DOCUMENTATION=no

    if ! OPTIONS=$(getopt --options 'uFl:afDj:h' --long 'unit-tests,check-format,lint:,all,format,documentation,jobs:,help' --name "$(basename "$0")" -- "$@"); then
        usage >&2
        failure
    fi
    eval set -- "$OPTIONS"
    unset OPTIONS

    while true; do
        case "$1" in
            '-u' | '--unit-tests')
                RUN_UNIT_TESTS=yes
                shift
                ;;
            '-F' | '--check-format')
                RUN_CHECK_FORMAT=yes
                shift
                ;;
            '-l' | '--lint')
                test "$2" = "all" && LINTERS="${ALL_LINTERS}" || LINTERS="$2"
                for LINTER in ${LINTERS//,/ }; do
                    case ,"${ALL_LINTERS}", in
                        *,"${LINTER}",*)
                            FLAG="RUN_${LINTER//-/_}"
                            eval "${FLAG^^}=yes"
                            ;;
                        *) failure "unknown linter: ${LINTER}" ;;
                    esac
                done
                shift 2
                ;;
            '-a' | '--all')
                RUN_UNIT_TESTS=yes
                RUN_CHECK_FORMAT=yes
                RUN_MYPY=yes
                RUN_RUFF=yes
                RUN_BANDIT=yes
                RUN_SEMGREP=yes
                shift
                ;;
            '-f' | '--format')
                RUN_FORMAT=yes
                shift
                ;;
            '-D' | '--documentation')
                RUN_DOCUMENTATION=yes
                shift
                ;;
            '-j' | '--jobs')
                JOBS="$2"
                shift 2
                ;;
            '-h' | '--help')
                usage
                exit 0
                ;;
            '--')
                shift
                test ${#@} -eq 0 || failure "extra arguments:" "$@"
                break
                ;;
            *) failure "internal error" ;;
        esac
    done

    readonly RUN_UNIT_TESTS RUN_CHECK_FORMAT RUN_MYPY RUN_RUFF RUN_BANDIT RUN_SEMGREP RUN_FORMAT RUN_DOCUMENTATION JOBS
}

setup_venv() {
    test "${SETUP_VENV_RAN}" = "yes" && return
    # TODO: Let rules_uv know about our toolchain... https://github.com/theoremlp/rules_uv/issues/163
    # Further: uv seems to fall back to clang, see https://github.com/astral-sh/uv/issues/8036
    CC="gcc" bazel run //:create_venv
    # shellcheck source=/dev/null
    source "$(bazel info workspace)"/.venv/bin/activate
    SETUP_VENV_RAN=yes
}

run_unit_tests() {
    bazel test :all
}

run_check_format() {
    bazel run //:format.check "$PWD"
}

run_mypy() {
    bazel build --config=mypy :all
}

run_ruff() {
    bazel lint --aspect:interactive=false --quiet --diff --machine :all
}

run_bandit() {
    setup_venv
    # We would like to put everything into pyproject.toml, so running just "bandit" would do the
    # right thing. But bandit is totally obscure & complicated regarding configuration, see several
    # issues like https://github.com/PyCQA/bandit/issues/606.  :-/
    bandit --configfile=pyproject.toml --quiet --recursive --severity-level=medium cmk tests
}

run_semgrep() {
    # semgrep requires a dedicated venv at the moment. We would rather have semgrep as regular dev
    # dependency, which would make the run-semgrep wrapper obsolete. See also:
    #   https://review.lan.tribe29.com/c/check_mk/+/92077
    #   https://github.com/semgrep/semgrep/issues/10408
    SEMGREP_DIR="$(bazel info workspace)/tests/semgrep"
    "${SEMGREP_DIR}"/run-semgrep scan \
        --config "${SEMGREP_DIR}/rules" \
        --error \
        --quiet \
        --oss-only \
        --use-git-ignore \
        --disable-version-check \
        cmk tests
}

run_format() {
    bazel run //:format "$PWD"
}

run_documentation() {
    echo "TODO: generate documentation"
}

main() {
    # Change to the directory where this script resides, it makes many things easier
    # and we can call this script from everywhere.
    cd -- "${BASH_SOURCE%/*}"
    parse_options "$@"
    test ${RUN_UNIT_TESTS} = yes && run_unit_tests
    test ${RUN_CHECK_FORMAT} = yes && run_check_format
    test ${RUN_MYPY} = yes && run_mypy
    test ${RUN_RUFF} = yes && run_ruff
    test ${RUN_BANDIT} = yes && run_bandit
    test ${RUN_SEMGREP} = yes && run_semgrep
    test ${RUN_DOCUMENTATION} = yes && run_documentation
    test ${RUN_FORMAT} = yes && run_format
    true
}

main "$@"
